<?php

/*
 * Copyright 2008 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#namespace tehframework\mvc;

#use \InvalidArgumentException;
#use \OutOfRangeException;

class ModelImpl implements Model
{
	/**
	 * @var array<string,mixed?>
	 */
	private $attributes;
	
	/**
	 * @param  Model|array<string,mixed?>? $attributes
	 */
	public function __construct($attributes = null)
	{
		if (empty($attributes))
		{
			$this->attributes = array();
		}
		else
		{
			$this->copyAttributes($attributes);
		}
	}
	
	/**
	 * Returns an array of all attributes.
	 * 
	 * @return array<string,mixed?>
	 *         An array of all attributes.
	 */
	public function getAttributeArray()
	{
		return $this->attributes;
	}
	
	/**
	 * Determines whether attribute with {@link $name} exists.
	 *
	 * @param  string $name
	 *         A name of the attribute to check.
	 * @return bool
	 *         TRUE if the attribute exists; FALSE otherwise.
	 */
	public function hasAttribute($name)
	{
		return array_key_exists($name, $this->attributes);
	}
	
	/**
	 * Returns a value of the existing attribute.
	 *
	 * @param  string $name
	 *         A name of the existing attribute to return.
	 * @return mixed
	 *         A value of the attribute.
	 * @throws OutOfRangeException
	 *         If there is no attribute with the specified {@link $name}.
	 */
	public function getAttribute($name)
	{
		if (!$this->hasAttribute($name))
		{
			throw new OutOfRangeException(sprintf(
				'Attribute [%s] does not exist.', $name
			));
		}
		
		return $this->attributes[$name];
	}
	
	/**
	 * Returns a value of the attribute or {@link $default}, if the specified
	 * attribute does not exist.
	 * 
	 * This method is an equivalent of ($model is an instance of Model):
	 * <code>
	 *   if ($model->hasAttribute($name))
	 *   {
	 *     $value = $model->getAttribute($name);
	 *   }
	 *   else
	 *   {
	 *     $value = $default;
	 *   }
	 * </code>
	 *
	 * @param  string $name
	 *         A name of the attribute to return.
	 * @param  mixed? $default
	 *         A value returned when the specified attribute does not exist.
	 * @return mixed?
	 *         A value of the attribute with the specified {@link $name}
	 *         or {@link $default} if it does not exist.
	 */
	public function tryGetAttribute($name, $default = null)
	{
		return $this->hasAttribute($name) ? $this->attributes[$name] : $default;
	}
	
	/**
	 * Sets a value of the {@link $name attribute} to {@link $value}.
	 * 
	 * If an attribute with the same {@link $name} already exists, its value
	 * will be overwritten.
	 *
	 * @param  string $name
	 *         A name of the attribute to set. Cannot be NULL or empty.
	 * @param  mixed? $value
	 *         A value to assign to the attribute whether it exists or not.
	 *         Can be NULL.
	 * @return ModelImpl
	 * @throws InvalidArgumentException
	 *         If {@link $name} is NULL or empty.
	 */
	public function addAttribute($name, $value)
	{
		if (empty($name))
		{
			throw new InvalidArgumentException('Name cannot be NULL or empty.');
		}
		
		$this->attributes[$name] = $value;
		
		return $this;
	}
	
	/**
	 * Copies all attributes from the specified {@link $model} to this model.
	 * 
	 * Attributes with the same names will be overwritten.
	 * 
	 * @param  Model|array<string,mixed?> $model
	 * @return ModelImpl
	 */
	public function copyAttributes($model)
	{
		if ($model instanceof Model)
		{
			$model = $model->getAttributeArray();
		}
		
		$this->attributes = array_merge($this->attributes, $model);
		
		return $this;
	}
	
	/**
	 * Removes an existing attribute with the specified {@link $name}.
	 *
	 * @param  string $name
	 *         A name of the existing attribute to remove.
	 * @return ModelImpl
	 * @throws OutOfRangeException
	 *         If the specified attribute does not exist.
	 */
	public function removeAttribute($name)
	{
		if (!$this->hasAttribute($name))
		{
			throw new OutOfRangeException(sprintf(
				'Attribute [%s] does not exist.', $name
			));
		}
		
		unset($this->attributes[$name]);
		
		return $this;
	}
	
	/**
	 * Removes all existing attributes.
	 * 
	 * @return ModelImpl
	 */
	public function clearAttributes()
	{
		$this->attributes = array();
		
		return $this;
	}
}